(function() {
    FlickrService = function(oConfig) {
        if(FlickrService.getInstance() != null)
            throw new Error("Only one FlickrService at a time, please.");
        if(typeof oConfig.apiKey == "undefined")
            throw new Error("Invalid arguments to FlickrService constructor.  Please specify an API key.");
        if(typeof oConfig.secret == "undefined")
            throw new Error("Invalid arguments to FlickrService constructor.  Please specify a shared secret.");
        if(oConfig.permissionLevel)
            this._permissionLevel = oConfig.permissionLevel;            
        this._apiKey = oConfig.apiKey;
        this._secret = oConfig.secret;
        this._loadCookieSettings();
        FlickrService._instance = this;
        window.onload = this._checkFrob.call(this);
    };
    
    FlickrService.prototype._apiKey             = null;
    FlickrService.prototype._secret             = null;
    FlickrService.prototype._query              = null;
    FlickrService.prototype._scriptTag          = null;
    FlickrService.prototype._authToken          = null;
    FlickrService.prototype._permissionLevel    = null;    
    FlickrService.prototype._user               = null;
    FlickrService.prototype._verifiedPerms      = null;

    FlickrService._instance                     = null;
    FlickrService._apiUrl                       = "http://api.flickr.com/services/rest/?";
    FlickrService._authUrl                      = "http://flickr.com/services/auth/?";
    FlickrService._scriptTagId                  = "flickrservice-jsonp";
    FlickrService._authCookieName               = "flickrservice_auth";
    FlickrService._userCookieName               = "flickrservice_user";    
    
    FlickrService.photoSizes = {
        smallSquare : "s", /* 75x75 */
        thumbnail   : "t", /* 100 on longest side */
        small       : "m", /* 240 on longest side */
        medium      : "",  /* 500 on longest side */
        large       : "b"  /* 1024 on longest side */
    };
    
    FlickrService._authCallback = function(oRes) {
        var self = FlickrService.getInstance(),
            stat = oRes.stat,
            token = null;
        if(stat == "fail") 
            throw new Error("Flickr API authentication error: " + oRes.message);
        else {
            token = oRes.auth.token._content;
            // set cookies to expire in 14 days
            self._setCookie({
                name: FlickrService._authCookieName,
                value: token + "|" + oRes.auth.perms._content,
                expires: 14
            });
            self._setCookie({
                name: FlickrService._userCookieName,
                value: oRes.auth.user.nsid + "|" + oRes.auth.user.username + "|" + oRes.auth.user.fullname,
                expires: 14
            });
            self._user = oRes.auth.user;
            self._verifiedPerms = oRes.auth.perms._content;
        }
    };

    FlickrService.getInstance = function() {
        return FlickrService._instance;
    };
    
    FlickrService.prototype._callMethod = function(sMethod, oConfig) {
        if(typeof oConfig.onComplete == "undefined")
            throw new Error("Invalid arguments to '" + sMethod + "'.  Please specify an onComplete callback method.");
        if(oConfig.onLoading) {
            if(typeof oConfig.onLoading != "function")
                throw new Error("Invalid arguments to '" + sMethod + "'.  onLoading parameter must be a function.");
            else
                oConfig.onLoading.apply(oConfig.onLoading);
        }
        if(oConfig.perms && !this._getAuthToken()) {
            this._authenticate(oConfig.perms);            
        }            
        else {
            this._buildQuery(sMethod, oConfig);
            this._sendRequest();            
        }
    };
    
    FlickrService.prototype._buildQuery = function(sMethod, oConfig) {
        var q = [],
            callback = null,            
            now = (new Date).getTime();        
        if(typeof oConfig.onComplete == "function") {            
            callback = "FlickrService.callback_"+now;
            FlickrService["callback_"+now] = oConfig.onComplete;
        } else {
            callback = oConfig.onComplete;
        }
        q.push("api_key="+this._apiKey);
        q.push("format=json");
        q.push("jsoncallback="+callback);
        q.push("method="+sMethod);
        delete oConfig.onComplete;
        delete oConfig.onLoading;
        for(o in oConfig) {
            if(oConfig.hasOwnProperty(o) && o != "perms")
                q.push(o+"="+escape(oConfig[o]));
        }
        this._query = FlickrService._apiUrl + q.join("&");    
        if(oConfig.perms || oConfig.signCall) {
            this._query += "&auth_token=" + this._getAuthToken();
            this._buildApiSig(q);
        }        
    };
    
    FlickrService.prototype._buildApiSig = function(aQuery) {
        aQuery.push("auth_token="+this._getAuthToken());
        aQuery.sort(function(a, b) {
            return a.split('=')[0] > b.split('=')[0];
        });
        this._query += "&api_sig=" + FlickrService.util.md5((this._secret + aQuery.join("")).replace(/=/g,""));
    };
    
    FlickrService.prototype._createScriptTag = function(sSrc) {
        this._scriptTag = document.createElement("script");
        this._scriptTag.type = "text/javascript";
        this._scriptTag.id = FlickrService._scriptTagId;
        this._scriptTag.src = sSrc;
    };
    
    FlickrService.prototype._sendRequest = function() {
        if(document.getElementById(FlickrService._scriptTagId)) {
            this._scriptTag.parentNode.removeChild(this._scriptTag);
        }        
        this._createScriptTag(this._query);
        document.body.appendChild(this._scriptTag);        
    };
    
    FlickrService.prototype._authenticate = function(sType) {
        var url = FlickrService._authUrl,
            perms = (this._permissionLevel == "delete") ? "delete" : (this._permissionLevel == "write") ? "write" : sType,
            input = this._secret + "api_key" + this._apiKey + "perms" + perms,
            apiSig = FlickrService.util.md5(input);
        url += "api_key=" + this._apiKey + "&perms=" + perms + "&api_sig=" + apiSig;
        window.location = url;
    };    
    
    FlickrService.prototype._getCookie = function(sName) {
        var cookieNameEq = sName + "=",
            cookies = document.cookie.split(';');
        for(var i=0, len=cookies.length; i<len; i++) {
            var cookie = cookies[i];
            while(cookie.charAt(0) == ' ')
                cookie = cookie.substring(1, cookie.length);
            if(cookie.indexOf(cookieNameEq) == 0)
                return cookie.substring(cookieNameEq.length, cookie.length);
        }
        return null;        
    };
    
    FlickrService.prototype._setCookie = function(oConfig) {
        var date = new Date(),
            expires = null;
        date.setTime(date.getTime() + oConfig.expires*24*60*60*60*1000); // set cookies to expire in 2 weeks
        expires = date.toGMTString();        
        document.cookie = oConfig.name + "=" + oConfig.value + "; expires=" + expires + "; path=/";
    };
    
    FlickrService.prototype._loadCookieSettings = function() { 
        if(this._getCookie(FlickrService._authCookieName)) {
            this._authToken = this._getAuthToken();
            this._verifiedPerms = this._getVerifiedPerms();
            if(this._getCookie(FlickrService._userCookieName)) {
                this._user = this.getUser();
            }
        }    
    };
    
    FlickrService.prototype._checkFrob = function() {
        var location = window.location,
            params = location.search;
        if(params.match(/frob=[a-zA-Z0-9]+/)) {
            var frob = params.match(/frob=([a-zA-Z0-9-_]+)/)[1];
            this._fetchAuthToken(frob);
        }
    };
    
    FlickrService.prototype._fetchAuthToken = function(sFrob) {
        var input = this._secret + "api_key" + this._apiKey + "formatjsonfrob" + sFrob + "jsoncallbackFlickrService._authCallbackmethodflickr.auth.getToken",
            apiSig = FlickrService.util.md5(input),
            url = "http://flickr.com/services/rest/?format=json&jsoncallback=FlickrService._authCallback&method=flickr.auth.getToken&api_key=4854b66d900b3214426f88b147d31b3f&frob=" + sFrob + "&api_sig=" + apiSig,
            scriptTag = document.createElement("script");
        scriptTag.type = "text/javascript";
        scriptTag.src = url;
        document.body.appendChild(scriptTag);
    },
    
    FlickrService.prototype._getAuthToken = function() {
        if(this._authToken != null)
            return this._authToken;
        var authCookie = this._getCookie(FlickrService._authCookieName);
        if(authCookie == null)
            return false;
        return authCookie.split('|')[0];
    };
    
    FlickrService.prototype._getVerifiedPerms = function() {
        if(this._verifiedPerms != null)
            return this._verifiedPerms;
        var authCookie = this._getCookie(FlickrService._authCookieName);
        if(authCookie == null)
            return false;
        return authCookie.split('|')[1];
    };
    
    FlickrService.prototype.getUser = function() {
        if(this._user != null)
            return this._user;
        var userCookie = this._getCookie(FlickrService._userCookieName);
        if(userCookie == null)
            return false;
        return {
            nsid: userCookie.split('|')[0],
            username: userCookie.split('|')[1],
            fullname: userCookie.split('|')[2]
        };
    };    
    
    FlickrService.prototype.deleteCookies = function() {
        this._setCookie({
            name: FlickrService._authCookieName,
            value: "",
            exipres: -1
        });
        this._setCookie({
            name: FlickrService._userCookieName,
            value: "",
            exipres: -1
        });        
    };
    
    FlickrService.prototype.getPhotoUrl = function(oPhoto, sSize) {
        var farm     = oPhoto.farm,
            server     = oPhoto.server,
            id         = oPhoto.id,
            secret    = oPhoto.secret,
            size     = null,
            url     = null;
        switch(sSize) {
            case FlickrService.photoSizes.smallSquare:
                size = FlickrService.photoSizes.smallSquare;
                break;
            case FlickrService.photoSizes.thumbnail:
                size = FlickrService.photoSizes.thumbnail;
                break;                
            case FlickrService.photoSizes.small:
                size = FlickrService.photoSizes.small;            
                break;                
            case FlickrService.photoSizes.medium:
                size = FlickrService.photoSizes.medium;            
                break;
            case FlickrService.photoSizes.large:
                size = FlickrService.photoSizes.large;            
                break;
            case null:
            case undefined:
                size = FlickrService.photoSizes.medium;
                break;
            default:
                throw new Error("Invalid size argument for photos.  Must be either s, t, m, b or <empty>.  See http://www.flickr.com/services/api/misc.urls.html");
                break;
        };
        if(size === FlickrService.photoSizes.medium)
            url = "http://farm"+farm+".static.flickr.com/"+server+"/"+id+"_"+secret+".jpg";
        else
            url = "http://farm"+farm+".static.flickr.com/"+server+"/"+id+"_"+secret+"_"+size+".jpg";
        return url;
    };
    
    FlickrService.prototype.activity = {
        
        /* Requires AUTH (read) */
        userComments: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.activity.userComments', oConfig);
        },

        /* Requires AUTH (read) */        
        userPhotos: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.activity.userPhotos', oConfig);
        }
        
    };
    
    FlickrService.prototype.auth = {
        
        checkToken: function(oConfig) {
            oConfig.signCall = true;
            return FlickrService.getInstance()._callMethod('flickr.auth.checkToken', oConfig);
        },
        
        getFrob: function(oConfig) {
            oConfig.signCall = true;
            return FlickrService.getInstance()._callMethod('flickr.auth.getFrob', oConfig);
        },
        
        getFullToken: function(oConfig) {
            oConfig.signCall = true;
            return FlickrService.getInstance()._callMethod('flickr.auth.getFullToken', oConfig);
        },        
        
        getToken: function(oConfig) {
            oConfig.signCall = true;
            return FlickrService.getInstance()._callMethod('flickr.auth.getToken', oConfig);
        }        
        
    };
    
    FlickrService.prototype.blogs = {

        /* Requires AUTH (read) */
        getList: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.blogs.getList', oConfig);            
        },
        
        /* Requires AUTH (write); Requires HTTP POST */
        postPhoto: function(oConfig) {

        }
        
    };
    
    FlickrService.prototype.contacts = {
        
        /* Requires AUTH (read) */
        getList: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.contacts.getList', oConfig);            
        },
        
        getPublicList: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.contacts.getPublicList', oConfig);            
        }
        
    };
    
    FlickrService.prototype.favorites = {
        
        /* Requires AUTH (write); Requires HTTP POST */
        add: function(oConfig) {

        },
        
        /* Requires AUTH (read) */
        getList: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.favorites.getList', oConfig);            
        },
        
        getPublicList: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.favorites.getPublicList', oConfig);            
        },
        
        /* Requires AUTH (write); Requires HTTP POST */
        remove: function(oConfig) {

        }
        
    };
    
    FlickrService.prototype.groups = {
        /* Requires AUTH (read) */
        browse: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.groups.browse', oConfig);            
        },
        
        getInfo: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.groups.getInfo', oConfig);            
        },
        
        search: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.groups.search', oConfig);            
        },
        
        pools: {
            
            /* Requires AUTH (write); Requires HTTP POST */
            add: function(oConfig) {

            },
            
            getContext: function(oConfig) {
                return FlickrService.getInstance()._callMethod('flickr.groups.pools.getContext', oConfig);
            },
            
            /* Requires AUTH (read) */
            getGroups: function(oConfig) {
                oConfig.perms = "read";
                return FlickrService.getInstance()._callMethod('flickr.groups.pools.getGroups', oConfig);            
            },
            
            getPhotos: function(oConfig) {
                return FlickrService.getInstance()._callMethod('flickr.groups.pools.getPhotos', oConfig);
            },
            
            /* Requires AUTH (write); Requires HTTP POST */
            remove: function(oConfig) {

            }        
            
        }
        
    };
    
    FlickrService.prototype.interestingness = {

        getList: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.interestingness.getList', oConfig);
        }
        
    };
    
    FlickrService.prototype.people = {
        
        findByEmail: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.people.findByEmail', oConfig);
        },
        
        findByUsername: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.people.findByUsername', oConfig);
        },
        
        getInfo: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.people.getInfo', oConfig);
        },        
        
        getPublicGroups: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.people.getPublicGroups', oConfig);
        },        
        
        getPublicPhotos: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.people.getPublicPhotos', oConfig);
        },        
        
        getUploadStatus: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.people.getUploadStatus', oConfig);
        },        
        
    };
    
    FlickrService.prototype.photos = {

        /* Requires AUTH (write); Requires HTTP POST */
        addTags: function(oConfig) {
            
        },
        
        /* Requires AUTH (write); Requires HTTP POST */
        /* Have to rename to deletePhoto b/c of Webkit's treatment of delete keyword */
        deletePhoto: function(oConfig) {
            
        },
        
        getAllContexts: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.photos.getAllContexts', oConfig);
        },        
        
        /* Requires AUTH (read) */
        getContactsPhotos: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.photos.getContactsPhotos', oConfig);
        },        
        
        getContactsPublicPhotos: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.photos.getContactsPublicPhotos', oConfig);
        },        
        
        getContext: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.photos.getContext', oConfig);
        },

        /* Requires AUTH (read) */
        getCounts: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.photos.getCounts', oConfig);            
        },
        
        getExif: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.photos.getExif', oConfig);
        },
        
        getFavorites: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.photos.getFavorites', oConfig);
        },
        
        getInfo: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.photos.getInfo', oConfig);
        },        
                
        /* Requires AUTH (read) */
        getNotInSet: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.photos.getNotInSet', oConfig);            
        },

        /* Requires AUTH (read) */
        getPerms: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.photos.getPerms', oConfig);            
        },

        getRecent: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.photos.getRecent', oConfig);
        },        

        getSizes: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.photos.getSizes', oConfig);
        },
        
        /* Requires AUTH (read) */
        getUntagged: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.photos.getUntagged', oConfig);            
        },                
        
        /* Requires AUTH (read) */
        getWithGeoData: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.photos.getWithGeoData', oConfig);            
        },
        
        /* Requires AUTH (read) */
        getWithoutGeoData: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.photos.getWithoutGeoData', oConfig);            
        },

        /* Requires AUTH (read) */
        getRecentlyUpdated: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.photos.getRecentlyUpdated', oConfig);            
        },
        
        /* Requires AUTH (write); Requires HTTP POST */
        removeTag: function(oConfig) {
            
        },

        search: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.photos.search', oConfig);
        },
        
        /* Requires AUTH (write); Requires HTTP POST */
        setContentType: function(oConfig) {
            
        },
        
        /* Requires AUTH (write); Requires HTTP POST */
        setDates: function(oConfig) {
            
        },
        
        /* Requires AUTH (write); Requires HTTP POST */
        setMeta: function(oConfig) {
            
        },

        /* Requires AUTH (write); Requires HTTP POST */
        setPerms: function(oConfig) {
            
        },        

        /* Requires AUTH (write); Requires HTTP POST */
        setSafetyLevel: function(oConfig) {
            
        },
        
        /* Requires AUTH (write); Requires HTTP POST */
        setTags: function(oConfig) {
            
        },
        
        comments: {
            /* Requires AUTH (write); Requires HTTP POST */
            addComment: function(oConfig) {

            },        
            
            /* Requires AUTH (write); Requires HTTP POST */
            deleteComment: function(oConfig) {

            },            
            
            /* Requires AUTH (write); Requires HTTP POST */
            editComment: function(oConfig) {

            },        
            
            getList: function(oConfig) {
                return FlickrService.getInstance()._callMethod('flickr.photos.comments.getList', oConfig);
            }
        },
        
        geo: {
            
            getLocation: function(oConfig) {
                return FlickrService.getInstance()._callMethod('flickr.photos.geo.getLocation', oConfig);
            },
            
            /* Require AUTH (read) */
            getPerms: function(oConfig) {
                oConfig.perms = "read";
                return FlickrService.getInstance()._callMethod('flickr.photos.geo.getPerms', oConfig);
            },
            
            /* Requires AUTH (write); Requires HTTP POST */
            removeLocation: function(oConfig) {

            },
            
            /* Requires AUTH (write); Requires HTTP POST */
            setLocation: function(oConfig) {

            },
            
            /* Requires AUTH (write); Requires HTTP POST */
            setPerms: function(oConfig) {

            }
            
        },
        
        licenses: {
            
            getInfo: function(oConfig) {
                return FlickrService.getInstance()._callMethod('flickr.photos.licenses.getInfo', oConfig);
            },            

            /* Requires AUTH (write); Requires HTTP POST */
            setLicense: function(oConfig) {

            }            
            
        },
        
        notes: {
            
            /* Requires AUTH (write); Requires HTTP POST */
            add: function(oConfig) {

            },
            
            /* Requires AUTH (write); Requires HTTP POST */
            /* Have to rename to deleteNote b/c of Webkit's treatment of delete keyword */
            deleteNote: function(oConfig) {

            },                        

            /* Requires AUTH (write); Requires HTTP POST */
            edit: function(oConfig) {

            }
            
        },
        
        transform: {

            /* Requires AUTH (write); Requires HTTP POST */
            rotate: function(oConfig) {

            }
            
        },
        
        upload: {
            
            checkTickets: function(oConfig) {
                return FlickrService.getInstance()._callMethod('flickr.photos.upload.checkTickets', oConfig);
            }
            
        }
            
    };
    
    FlickrService.prototype.photosets = {
        
        /* Requires AUTH (write); Requires HTTP POST */
        addPhoto: function(oConfig) {

        },
        
        /* Requires AUTH (write); Requires HTTP POST */
        create: function(oConfig) {

        },    
        
        /* Requires AUTH (write); Requires HTTP POST */
        /* Have to rename to deletePhotoset b/c of Webkit's treatment of delete keyword */
        deletePhotoset: function(oConfig) {

        },
        
        /* Requires AUTH (write); Requires HTTP POST */
        editMeta: function(oConfig) {

        },    

        /* Requires AUTH (write); Requires HTTP POST */
        editPhotos: function(oConfig) {

        },    
        
        getContext: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.photosets.getContext', oConfig);
        },

        getInfo: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.photosets.getInfo', oConfig);
        },        
        
        getList: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.photosets.getList', oConfig);
        },        
        
        getPhotos: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.photosets.getPhotos', oConfig);
        },    
        
        /* Requires AUTH (write); Requires HTTP POST */
        orderSets: function(oConfig) {

        },        
        
        /* Requires AUTH (write); Requires HTTP POST */
        removePhoto: function(oConfig) {

        },
        
        comments: {

            /* Requires AUTH (write); Requires HTTP POST */
            addComment: function(oConfig) {

            },        
            
            /* Requires AUTH (write); Requires HTTP POST */
            deleteComment: function(oConfig) {

            },        
                        
            /* Requires AUTH (write); Requires HTTP POST */
            editComment: function(oConfig) {

            },        
            
            /* Requires AUTH (write); Requires HTTP POST */
            getList: function(oConfig) {

            }    
            
        }
        
    };
    
    FlickrService.prototype.places = {

        find: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.places.find', oConfig);
        },    
        
        findByLatLon: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.places.findByLatLon', oConfig);
        },    
        
        resolvePlaceId: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.places.resolvePlaceId', oConfig);
        },
        
        resolvePlaceUrl: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.places.resolvePlaceUrl', oConfig);
        }
        
    };
    
    FlickrService.prototype.prefs = {
        
        /* Requires AUTH (read) */
        getContentType: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.prefs.getContentType', oConfig);
        },
        
        /* Requires AUTH (read) */
        getGeoPerms: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.prefs.getGeoPerms', oConfig);
        },        
        
        /* Requires AUTH (read) */
        getHidden: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.prefs.getHidden', oConfig);
        },        
        
        /* Requires AUTH (read) */
        getPrivacy: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.prefs.getPrivacy', oConfig);
        },        
        
        /* Requires AUTH (read) */
        getSafetyLevel: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.prefs.getSafetyLevel', oConfig);
        }        
        
    };
    
    FlickrService.prototype.reflection = {
        
        getMethodInfo: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.reflection.getMethodInfo', oConfig);
        },

        getMethods: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.reflection.getMethods', oConfig);
        }        
        
    };
    
    FlickrService.prototype.tags = {
        
        getHotList: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.tags.getHotList', oConfig);
        },        
        
        getListPhoto: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.tags.getListPhoto', oConfig);
        },        
        
        getListUser: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.tags.getListUser', oConfig);
        },        
        
        getListUserPopular: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.tags.getListUserPopular', oConfig);
        },        
        
        getListUserRaw: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.tags.getListUserRaw', oConfig);
        },        
        
        getRelated: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.tags.getRelated', oConfig);
        }    
        
    };
    
    FlickrService.prototype.test = {
        
        echo: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.test.echo', oConfig);
        },        
        
        /* Requires AUTH (read) */
        login: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.test.login', oConfig);
        },
        
        /* Requires AUTH (read) */
        /* NOTE: changing name from null to testNull -> safari keyword */
        testNull: function(oConfig) {
            oConfig.perms = "read";
            return FlickrService.getInstance()._callMethod('flickr.test.null', oConfig);
        }    
        
    };
    
    FlickrService.prototype.urls = {
        
        getGroup: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.urls.getGroup', oConfig);
        },
        
        getUserPhotos: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.urls.getUserPhotos', oConfig);
        },        
        
        getUserProfile: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.urls.getUserProfile', oConfig);
        },        
        
        lookupGroup: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.urls.lookupGroup', oConfig);
        },        
        
        lookupUser: function(oConfig) {
            return FlickrService.getInstance()._callMethod('flickr.urls.lookupUser', oConfig);
        }
        
    };
    
    // http://kevin.vanzonneveld.net
    // +   original by: Webtoolkit.info (http://www.webtoolkit.info/)
    // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)        
    FlickrService.util = {
        utf8_encode: function(str_data) {
            str_data = str_data.replace(/\r\n/g,"\n");
            var tmp_arr = [], ac = 0;

            for (var n = 0; n < str_data.length; n++) {
                var c = str_data.charCodeAt(n);
                if (c < 128) {
                    tmp_arr[ac++] = String.fromCharCode(c);
                } else if((c > 127) && (c < 2048)) {
                    tmp_arr[ac++] = String.fromCharCode((c >> 6) | 192);
                    tmp_arr[ac++] = String.fromCharCode((c & 63) | 128);
                } else {
                    tmp_arr[ac++] = String.fromCharCode((c >> 12) | 224);
                    tmp_arr[ac++] = String.fromCharCode(((c >> 6) & 63) | 128);
                    tmp_arr[ac++] = String.fromCharCode((c & 63) | 128);
                }
            }

            return tmp_arr.join('');        
        },

        md5: function(str) {
            var RotateLeft = function(lValue, iShiftBits) {
                    return (lValue<<iShiftBits) | (lValue>>>(32-iShiftBits));
                };

            var AddUnsigned = function(lX,lY) {
                    var lX4,lY4,lX8,lY8,lResult;
                    lX8 = (lX & 0x80000000);
                    lY8 = (lY & 0x80000000);
                    lX4 = (lX & 0x40000000);
                    lY4 = (lY & 0x40000000);
                    lResult = (lX & 0x3FFFFFFF)+(lY & 0x3FFFFFFF);
                    if (lX4 & lY4) {
                        return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
                    }
                    if (lX4 | lY4) {
                        if (lResult & 0x40000000) {
                            return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
                        } else {
                            return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
                        }
                    } else {
                        return (lResult ^ lX8 ^ lY8);
                    }
                };

            var F = function(x,y,z) { return (x & y) | ((~x) & z); };
            var G = function(x,y,z) { return (x & z) | (y & (~z)); };
            var H = function(x,y,z) { return (x ^ y ^ z); };
            var I = function(x,y,z) { return (y ^ (x | (~z))); };

            var FF = function(a,b,c,d,x,s,ac) {
                    a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac));
                    return AddUnsigned(RotateLeft(a, s), b);
                };

            var GG = function(a,b,c,d,x,s,ac) {
                    a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac));
                    return AddUnsigned(RotateLeft(a, s), b);
                };

            var HH = function(a,b,c,d,x,s,ac) {
                    a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac));
                    return AddUnsigned(RotateLeft(a, s), b);
                };

            var II = function(a,b,c,d,x,s,ac) {
                    a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac));
                    return AddUnsigned(RotateLeft(a, s), b);
                };

            var ConvertToWordArray = function(str) {
                    var lWordCount;
                    var lMessageLength = str.length;
                    var lNumberOfWords_temp1=lMessageLength + 8;
                    var lNumberOfWords_temp2=(lNumberOfWords_temp1-(lNumberOfWords_temp1 % 64))/64;
                    var lNumberOfWords = (lNumberOfWords_temp2+1)*16;
                    var lWordArray=Array(lNumberOfWords-1);
                    var lBytePosition = 0;
                    var lByteCount = 0;
                    while ( lByteCount < lMessageLength ) {
                        lWordCount = (lByteCount-(lByteCount % 4))/4;
                        lBytePosition = (lByteCount % 4)*8;
                        lWordArray[lWordCount] = (lWordArray[lWordCount] | (str.charCodeAt(lByteCount)<<lBytePosition));
                        lByteCount++;
                    }
                    lWordCount = (lByteCount-(lByteCount % 4))/4;
                    lBytePosition = (lByteCount % 4)*8;
                    lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80<<lBytePosition);
                    lWordArray[lNumberOfWords-2] = lMessageLength<<3;
                    lWordArray[lNumberOfWords-1] = lMessageLength>>>29;
                    return lWordArray;
                };

            var WordToHex = function(lValue) {
                    var WordToHexValue="",WordToHexValue_temp="",lByte,lCount;
                    for (lCount = 0;lCount<=3;lCount++) {
                        lByte = (lValue>>>(lCount*8)) & 255;
                        WordToHexValue_temp = "0" + lByte.toString(16);
                        WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length-2,2);
                    }
                    return WordToHexValue;
                };

            var x=Array();
            var k,AA,BB,CC,DD,a,b,c,d;
            var S11=7, S12=12, S13=17, S14=22;
            var S21=5, S22=9 , S23=14, S24=20;
            var S31=4, S32=11, S33=16, S34=23;
            var S41=6, S42=10, S43=15, S44=21;

            str = FlickrService.util.utf8_encode(str);
            x = ConvertToWordArray(str);
            a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476;

            for (k=0;k<x.length;k+=16) {
                AA=a; BB=b; CC=c; DD=d;
                a=FF(a,b,c,d,x[k+0], S11,0xD76AA478);
                d=FF(d,a,b,c,x[k+1], S12,0xE8C7B756);
                c=FF(c,d,a,b,x[k+2], S13,0x242070DB);
                b=FF(b,c,d,a,x[k+3], S14,0xC1BDCEEE);
                a=FF(a,b,c,d,x[k+4], S11,0xF57C0FAF);
                d=FF(d,a,b,c,x[k+5], S12,0x4787C62A);
                c=FF(c,d,a,b,x[k+6], S13,0xA8304613);
                b=FF(b,c,d,a,x[k+7], S14,0xFD469501);
                a=FF(a,b,c,d,x[k+8], S11,0x698098D8);
                d=FF(d,a,b,c,x[k+9], S12,0x8B44F7AF);
                c=FF(c,d,a,b,x[k+10],S13,0xFFFF5BB1);
                b=FF(b,c,d,a,x[k+11],S14,0x895CD7BE);
                a=FF(a,b,c,d,x[k+12],S11,0x6B901122);
                d=FF(d,a,b,c,x[k+13],S12,0xFD987193);
                c=FF(c,d,a,b,x[k+14],S13,0xA679438E);
                b=FF(b,c,d,a,x[k+15],S14,0x49B40821);
                a=GG(a,b,c,d,x[k+1], S21,0xF61E2562);
                d=GG(d,a,b,c,x[k+6], S22,0xC040B340);
                c=GG(c,d,a,b,x[k+11],S23,0x265E5A51);
                b=GG(b,c,d,a,x[k+0], S24,0xE9B6C7AA);
                a=GG(a,b,c,d,x[k+5], S21,0xD62F105D);
                d=GG(d,a,b,c,x[k+10],S22,0x2441453);
                c=GG(c,d,a,b,x[k+15],S23,0xD8A1E681);
                b=GG(b,c,d,a,x[k+4], S24,0xE7D3FBC8);
                a=GG(a,b,c,d,x[k+9], S21,0x21E1CDE6);
                d=GG(d,a,b,c,x[k+14],S22,0xC33707D6);
                c=GG(c,d,a,b,x[k+3], S23,0xF4D50D87);
                b=GG(b,c,d,a,x[k+8], S24,0x455A14ED);
                a=GG(a,b,c,d,x[k+13],S21,0xA9E3E905);
                d=GG(d,a,b,c,x[k+2], S22,0xFCEFA3F8);
                c=GG(c,d,a,b,x[k+7], S23,0x676F02D9);
                b=GG(b,c,d,a,x[k+12],S24,0x8D2A4C8A);
                a=HH(a,b,c,d,x[k+5], S31,0xFFFA3942);
                d=HH(d,a,b,c,x[k+8], S32,0x8771F681);
                c=HH(c,d,a,b,x[k+11],S33,0x6D9D6122);
                b=HH(b,c,d,a,x[k+14],S34,0xFDE5380C);
                a=HH(a,b,c,d,x[k+1], S31,0xA4BEEA44);
                d=HH(d,a,b,c,x[k+4], S32,0x4BDECFA9);
                c=HH(c,d,a,b,x[k+7], S33,0xF6BB4B60);
                b=HH(b,c,d,a,x[k+10],S34,0xBEBFBC70);
                a=HH(a,b,c,d,x[k+13],S31,0x289B7EC6);
                d=HH(d,a,b,c,x[k+0], S32,0xEAA127FA);
                c=HH(c,d,a,b,x[k+3], S33,0xD4EF3085);
                b=HH(b,c,d,a,x[k+6], S34,0x4881D05);
                a=HH(a,b,c,d,x[k+9], S31,0xD9D4D039);
                d=HH(d,a,b,c,x[k+12],S32,0xE6DB99E5);
                c=HH(c,d,a,b,x[k+15],S33,0x1FA27CF8);
                b=HH(b,c,d,a,x[k+2], S34,0xC4AC5665);
                a=II(a,b,c,d,x[k+0], S41,0xF4292244);
                d=II(d,a,b,c,x[k+7], S42,0x432AFF97);
                c=II(c,d,a,b,x[k+14],S43,0xAB9423A7);
                b=II(b,c,d,a,x[k+5], S44,0xFC93A039);
                a=II(a,b,c,d,x[k+12],S41,0x655B59C3);
                d=II(d,a,b,c,x[k+3], S42,0x8F0CCC92);
                c=II(c,d,a,b,x[k+10],S43,0xFFEFF47D);
                b=II(b,c,d,a,x[k+1], S44,0x85845DD1);
                a=II(a,b,c,d,x[k+8], S41,0x6FA87E4F);
                d=II(d,a,b,c,x[k+15],S42,0xFE2CE6E0);
                c=II(c,d,a,b,x[k+6], S43,0xA3014314);
                b=II(b,c,d,a,x[k+13],S44,0x4E0811A1);
                a=II(a,b,c,d,x[k+4], S41,0xF7537E82);
                d=II(d,a,b,c,x[k+11],S42,0xBD3AF235);
                c=II(c,d,a,b,x[k+2], S43,0x2AD7D2BB);
                b=II(b,c,d,a,x[k+9], S44,0xEB86D391);
                a=AddUnsigned(a,AA);
                b=AddUnsigned(b,BB);
                c=AddUnsigned(c,CC);
                d=AddUnsigned(d,DD);
            }

            var temp = WordToHex(a)+WordToHex(b)+WordToHex(c)+WordToHex(d);

            return temp.toLowerCase();        
        }    
    };    
})();